Skip to content

server: hash setup tokens at rest#280

Closed
erikolsson wants to merge 1 commit into
mainfrom
security/hash-setup-tokens
Closed

server: hash setup tokens at rest#280
erikolsson wants to merge 1 commit into
mainfrom
security/hash-setup-tokens

Conversation

@erikolsson

@erikolsson erikolsson commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • setupTokens.token was stored plaintext and looked up by raw equality. A read-only DB leak (backup, replica, query log) inside the 10-minute expiry would have handed an attacker every pending pairing token, redeemable for a long-lived device API key.
  • Switch to the same SHA-256-at-rest pattern that apiKeys.keyHash, refresh sessions, and MCP OAuth tokens already use: hash on issuance, hash the incoming token on /v1/auth/exchange, compare hashes.
  • Raw token is returned to the caller exactly once (in the issuance response) and never re-read from the DB.

Compatibility

  • The setup_tokens.token column type is unchanged (text); this is a behavior-only change. No migration required.
  • Any unredeemed setup tokens issued before this lands stop working — they'd compare a UUID against a SHA-256 hex string. Acceptable: tokens expire in 10 minutes, users re-generate. No production communication needed beyond the deploy window.

Audit context

First of three PRs from a security review of the desktop pairing flow. PR 2 will close the TOCTOU on redemption (atomic UPDATE … RETURNING); PR 3 will add a unique index on api_keys.device_id so a future regression can never leave two valid keys per device.

Test plan

  • bun run typecheck (apps/server) — clean
  • bun run test (apps/server) — 290 pass / 0 fail
  • Manual: generate a setup token, redeem on desktop, confirm pairing still works end-to-end

🤖 Generated with Claude Code


Note

Medium Risk
Touches the device pairing/auth exchange path: newly issued setup tokens are now hashed and exchange compares hashes, which can break any in-flight tokens during deploy and could block pairing if hashing/lookup mismatches occur.

Overview
Setup tokens used for device/CLI pairing are no longer stored in plaintext: POST /api/me/setup-token now saves a SHA-256 hash while returning the raw token only in the response.

POST /v1/auth/exchange now hashes the presented token and looks up setupTokens by hash before redeeming and issuing the device API key.

Reviewed by Cursor Bugbot for commit 5fc9830. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Improved token security in setup token generation and authentication exchange flows by enhancing how tokens are stored and validated within the authentication system.

Setup tokens were stored plaintext and looked up by raw equality, so a
read-only DB leak inside the 10-minute expiry window would expose every
pending pairing token. Switch to the same SHA-256-at-rest pattern that
apiKeys.keyHash, refresh sessions, and MCP OAuth tokens already use:
hash on issuance, hash incoming token on /v1/auth/exchange, compare hashes.

Raw token is returned to the caller exactly once and never re-read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Walkthrough

This pull request implements token hashing for setup tokens across two endpoints. The /api/me/setup-token endpoint now hashes the generated setup token before persisting it to the database, and the POST /v1/auth/exchange endpoint now hashes the incoming token before querying for a matching setup token. The raw token is returned once to the client in the initial response but is never stored plaintext. The public API responses and overall exchange flow remain unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'server: hash setup tokens at rest' directly and concisely describes the main security change: implementing SHA-256 hashing of setup tokens at rest instead of storing them in plaintext.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/hash-setup-tokens

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/server/src/auth/github.ts (1)

266-278: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make setup-token redemption atomic before release.

Lines 267-278 still perform a separate read (redeemed = false) and then update. Two concurrent exchanges can both pass the read and both mint API keys. Use a single conditional redeem operation (e.g., atomic update with redeem guard + expiry guard and row return) so only one request can succeed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/auth/github.ts` around lines 266 - 278, The current flow
hashes the token, selects a row from setupTokens with redeemed = false and then
separately updates redeemed = true, which allows a race where two requests can
both read before either update; instead perform a single atomic conditional
redeem: hashToken(body.token) then run one db.update on setupTokens that sets
redeemed = true with a WHERE that matches token = tokenHash AND redeemed = false
AND expiresAt > new Date() and RETURN the updated row (or check affected row
count) so you can detect success/failure atomically; replace the separate
db.select + db.update (references: hashToken, setupTokens, db.update, db.select,
st, redeemed, expiresAt) with this single conditional update and handle the case
where no row is returned/affected as the invalid/expired token path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/server/src/auth/github.ts`:
- Around line 266-278: The current flow hashes the token, selects a row from
setupTokens with redeemed = false and then separately updates redeemed = true,
which allows a race where two requests can both read before either update;
instead perform a single atomic conditional redeem: hashToken(body.token) then
run one db.update on setupTokens that sets redeemed = true with a WHERE that
matches token = tokenHash AND redeemed = false AND expiresAt > new Date() and
RETURN the updated row (or check affected row count) so you can detect
success/failure atomically; replace the separate db.select + db.update
(references: hashToken, setupTokens, db.update, db.select, st, redeemed,
expiresAt) with this single conditional update and handle the case where no row
is returned/affected as the invalid/expired token path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24177cba-1fc0-4819-9fbd-d237c66e5c70

📥 Commits

Reviewing files that changed from the base of the PR and between 719dcbb and 5fc9830.

📒 Files selected for processing (2)
  • apps/server/src/auth/github.ts
  • apps/server/src/user/routes.ts

giuseppecrj added a commit that referenced this pull request May 5, 2026
## Summary

- Combines the security fixes from #280, #281, and #282 on top of
`g/consolidate-server-boundaries` so those separate PRs can be closed
after review.
- Hashes setup tokens at rest while still returning the raw token once
for pairing.
- Claims setup tokens atomically during `/v1/auth/exchange` so
concurrent redemption cannot mint multiple API keys.
- Adds a unique index on `api_keys(device_id)` as the schema-level
one-active-key-per-device guarantee.
- Updates tests for the new invariant and adds an assertion that raw
setup tokens are not stored in the database.

## Validation

- `bun run gen:db-schema:check`
- `bun run typecheck`
- `bun test test/auth-resolvers.test.ts`
- `bun run test` — 296 pass, 0 fail

## Notes

This PR targets `g/consolidate-server-boundaries` intentionally. Once it
lands into the consolidation branch, the older security PRs #280, #281,
and #282 should be redundant.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes security-sensitive auth flows and adds a data-cleanup
migration with a new uniqueness constraint on `api_keys`, which could
affect existing tokens/devices if assumptions are wrong.
> 
> **Overview**
> **Hardens the setup-token pairing flow** by storing only a SHA-256
hash of setup tokens and updating `/v1/auth/exchange` to *atomically*
claim (redeem) tokens while also checking expiry.
> 
> **Enforces one active API key per device** by adding a unique index on
`api_keys.device_id` (with a migration that deletes older duplicate keys
per device) and updating tests/docs to match the new hashing and
uniqueness invariants.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6cede0e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed race condition in setup token redemption that could create
duplicate API keys
  * Enforced one-API-key-per-device constraint

* **Security**
  * Setup tokens are now securely hashed before storage

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Erik Olsson <valross2@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@giuseppecrj

Copy link
Copy Markdown
Contributor

closing due to #289

@giuseppecrj giuseppecrj closed this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants